package bsh;

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.LinkedList;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import bsh.AssignmentNodeCompiler.AssignmentTarget;
import bsh.asm.acessor.DynamicFieldAccessor;
import bsh.asm.acessor.ObjectAccessor;
import bsh.exception.CompilationAssertionFailed;
import bsh.exception.CompilationFailedException;
import bsh.meta.JavaClassWrapper;
import bsh.meta.MethodDescriptor;
import bsh.meta.Scope;
import bsh.meta.TypeWrapper;
import bsh.utils.reflection.BytecodeHelper;

/**
 * compilador para {@link BSHUnaryExpression}
 * 
 * @author takeshi
 */
public class UnaryOperationNodeCompiler implements NodeCompiler {

    private static final int NUM_3 = 3;

    private final BSHUnaryExpression node;

    private final CompilationUnit unit;

    /**
     * @param node {@link BSHUnaryExpression}
     * @param unit {@link CompilationUnit}
     */
    public UnaryOperationNodeCompiler(BSHUnaryExpression node, CompilationUnit unit) {
        super();
        this.node = node;
        this.unit = unit;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.NodeCompiler#compile(org.objectweb.asm.MethodVisitor, bsh.CompilationState)
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        if (this.node.kind == BSHUnaryExpression.PLUS) {
            // nothing to compile
            return;
        }
        state.addLineNumber(host, this.node.getLineNumber());
        boolean shouldStore = this.node.kind == BSHUnaryExpression.INCR || this.node.kind == BSHUnaryExpression.DECR;
        if (shouldStore) {
            this.handleStoring(host, state);
        } else {
            int old = state.getStackSize();
            this.unit.getNodeCompiler(this.node.getChild(0)).compile(host, state);
            // checkStackTopUnboxWrappers(host, state, node.getLineNumber(),
            // unit);
            TypeWrapper top = state.peekStack();
            Class<?> type = JavaClassWrapper.unwrapJavaClass(top);
            if (type == null) {
                throw new CompilationFailedException(this.node.getLineNumber(), "Expected java type but was: " + top);
            }
            if (Primitive.isWrapperType(type)) {
                this.unit.getCompilerManager().unbox(type, host, state);
                this.executeOperation(host, state);
            } else if (type.isPrimitive()) {
                this.executeOperation(host, state);
            } else if (BigDecimal.class.equals(type)) {
                this.executeBigDecimalOperation(host, state);
            } else {
                this.performGenericMethod(host, state);
            }
            state.assertStackSize(old + 1);
        }
    }

    /**
     * Invoca o metodo {@link CompiledScriptHelper#performUnaryOperation(Object, int, int, Prototype)}
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void performGenericMethod(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        MethodDescriptor md = this.unit.reflectionUtils().getMethodWithName(CompiledScriptHelper.class, "performUnaryOperation");
        int id = this.unit.thisType().newDynamicInvocationId();
        BytecodeHelper.pushIntConst(id, host);
        state.pushStack(int.class);
        BytecodeHelper.pushIntConst(this.node.kind, host);
        state.pushStack(int.class);
        pushThis(host, state);
        md.writeInvoke(host, state);
    }

    /**
     * Compila uma operacao de bigdecimal
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void executeBigDecimalOperation(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        final TypeWrapper top = state.peekStack();
        switch (this.node.kind) {
        case BSHUnaryExpression.INCR:
            // push bigdecimal.one
            this.pushBigDecimalOne(host, state);
            pushMathContext(state, host);
            host.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(BigDecimal.class), "add", bigDecimalArithmeticMethod());
            state.popStack();
            state.popStack();
            // state.pushStack(BigDecimal.class);
            break;

        case BSHUnaryExpression.DECR:
            this.assertIsNumber(top);
            // opcode = top.toASMType().getOpcode(Opcodes.IADD);
            this.pushBigDecimalOne(host, state);
            pushMathContext(state, host);
            host.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(BigDecimal.class), "subtract", bigDecimalArithmeticMethod());
            state.popStack();
            state.popStack();
            // state.pushStack(top);
            break;

        case BSHUnaryExpression.MINUS:
            this.assertIsNumber(top);
            host.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(BigDecimal.class), "negate",
                    Type.getMethodDescriptor(Type.getType(BigDecimal.class), new Type[]{}));
            break;

        case BSHUnaryExpression.PLUS:
            // well do nothing
            break;

        default:
            throw new CompilationAssertionFailed("Not yet implememented");
        }
    }

    /**
     * @param state {@link CompilationState}
     * @param host {@link MethodVisitor}
     * @throws CompilationFailedException e
     */
    public static void pushMathContext(CompilationState state, MethodVisitor host) throws CompilationFailedException {
        host.visitVarInsn(Opcodes.ALOAD, 0);
        state.pushStack(CompiledScript.class);
        MethodDescriptor descriptor =
                state.getUnit().getCompilerManager().reflectionUtils().getMethodWithName(CompiledScript.class, "getMathContext");
        descriptor.writeInvoke(host, state);
        state.assertStackTopType(JavaClassWrapper.wrapperFor(MathContext.class));
    }

    /**
     * @return String
     */
    public static String bigDecimalArithmeticMethod() {
        return Type.getMethodDescriptor(Type.getType(BigDecimal.class),
                new Type[]{ Type.getType(BigDecimal.class), Type.getType(MathContext.class) });
    }

    /**
     * Empurra o {@link BigDecimal#ONE} para o stack
     * 
     * @param host MethodVisitor
     * @param state {@link CompilationState}
     */
    private void pushBigDecimalOne(MethodVisitor host, CompilationState state) {
        host.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(BigDecimal.class), "ONE", Type.getDescriptor(BigDecimal.class));
        state.pushStack(BigDecimal.class);
    }

    /**
     * Empurra o 'this' para o stack
     * 
     * @param host MethodVisitor
     * @param state CompilationState
     */
    public static void pushThis(MethodVisitor host, CompilationState state) {
        host.visitVarInsn(Opcodes.ALOAD, 0);
        state.pushStack(CompiledScript.class);
    }

    /**
     * Compila expressoes do tipo a++ ou ++a
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void handleStoring(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        int old = state.getStackSize();
        BSHPrimaryExpression exp = (BSHPrimaryExpression) this.node.getChild(0);
        final int lastIndex = exp.jjtGetNumChildren() - 1;
        for (int i = 0; i < lastIndex; i++) {
            this.unit.getNodeCompiler(exp.getChild(i)).compile(host, state);
        }
        AssignmentTarget target = AssignmentNodeCompiler.createAssignmentTargetFor(exp.getChild(lastIndex), state, this.unit);
        final boolean local = this.isLocalVariable(target);
        target.prepare(host, state);
        target.dup(host, state);
        target.load(host, state);
        checkStackTopUnboxWrappers(host, state, this.node.getLineNumber(), this.unit);
        state.assertMinimumStackSize(old + 1);
        if (this.node.postfix) {
            // on postfix, we dup_x1 the value before the operator
            if (local) {
                host.visitInsn(Opcodes.DUP);
                state.pushStack(state.peekStack());
            } else if (isDynamicStorage(target)) {
                dupX2(host, state);
            } else {
                this.dupX(host, state);
            }
        }
        old = state.getStackSize();
        if (state.peekStack().isPrimitive()) {
            this.executeOperation(host, state);
        } else if (state.peekStack().equals(JavaClassWrapper.wrapperFor(BigDecimal.class))) {
            this.executeBigDecimalOperation(host, state);
        } else {
            this.performGenericMethod(host, state);
        }
        state.assertStackSize(old);
        state.assertMinimumStackSize(1);
        this.unit.getCompilerManager().coerceStackTopTo(host, target.type(), state);
        if (!this.node.postfix) {
            // on preffix, we dup_x1 the value after the operator(?)
            if (local) {
                host.visitInsn(Opcodes.DUP);
                state.pushStack(state.peekStack());
            } else if (isDynamicStorage(target)) {
                dupX2(host, state);
            } else {
                this.dupX(host, state);
            }
        }

        target.doAssign(host, state);
        state.assertMinimumStackSize(1);
    }

    /**
     * Compiles a DUP_X2 instruction, checking and mantaining the {@link CompilationState}
     * 
     * @param host the {@link MethodVisitor}
     * @param state {@link CompilationState}
     */
    public static void dupX2(MethodVisitor host, CompilationState state) {
        final int min = 3;
        state.assertMinimumStackSize(min);
        TypeWrapper top = state.popStack();
        TypeWrapper prev = state.popStack();
        TypeWrapper prev2 = state.popStack();
        state.pushStack(top);
        state.pushStack(prev2);
        state.pushStack(prev);
        state.pushStack(top);
        state.assertMinimumStackSize(NUM_3 + 1);
        host.visitInsn(Opcodes.DUP_X2);
    }

    /**
     * Verifies if an {@link AssignmentTarget} is to a {@link DynamicFieldAccessor}
     * 
     * @param target {@link AssignmentTarget}
     * @return boolean
     */
    private boolean isDynamicStorage(AssignmentTarget target) {
        if (target instanceof AmbiguousNameAssignmentTarget) {
            LinkedList<ObjectAccessor> list = ((AmbiguousNameAssignmentTarget) target).getList();
            return list.get(list.size() - 1) instanceof DynamicFieldAccessor;
        }
        return false;
    }

    /**
     * Verifica se um {@link AssignmentTarget} eh uma variavel local
     * 
     * @param target {@link AssignmentTarget}
     * @return boolean
     */
    private boolean isLocalVariable(AssignmentTarget target) {
        if (target instanceof AmbiguousNameAssignmentTarget) {
            return ((AmbiguousNameAssignmentTarget) target).isLocal();
        }
        return false;
    }

    /**
     * Faz umbox de um primitive wrapper
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param lineNumber numero da linha
     * @param unit {@link CompilationUnit}
     * @throws CompilationFailedException e
     */
    public static void checkStackTopUnboxWrappers(MethodVisitor host, CompilationState state, int lineNumber, CompilationUnit unit)
            throws CompilationFailedException {
        TypeWrapper top = state.peekStack();
        Class<?> type = JavaClassWrapper.unwrapJavaClass(top);
        if (type == null) {
            throw new CompilationFailedException(lineNumber, "Expected java type but was: " + top);
        }

        if (Primitive.isWrapperType(type)) {
            int pre = state.getStackSize();
            unit.getCompilerManager().unbox(type, host, state);
            state.assertStackSize(pre);
        }
    }

    /**
     * Faz um DUP_X1 no stack do {@link CompilationState}. o DUP_X1 duplica o topo do stack e coloca na antepenultima posicao. Ex:
     * <p>
     * Antes:
     * <p>
     * A :: B
     * <p>
     * Depois:
     * <p>
     * A :: B :: A
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     */
    private void dupX(MethodVisitor host, CompilationState state) {
        state.assertMinimumStackSize(2);
        TypeWrapper top = state.popStack();
        TypeWrapper prev = state.popStack();
        state.pushStack(top);
        state.pushStack(prev);
        state.pushStack(top);
        state.assertMinimumStackSize(NUM_3);
        host.visitInsn(Opcodes.DUP_X1);
    }

    /**
     * Compila uma operacao numerica unaria
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void executeOperation(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        final TypeWrapper top = state.peekStack();
        int opcode;
        switch (this.node.kind) {
        case BSHUnaryExpression.INCR:
            this.assertIsNumber(top);
            opcode = top.toASMType().getOpcode(Opcodes.IADD);
            this.pushOne(host, state);
            host.visitInsn(opcode);
            state.popStack();
            state.popStack();
            state.pushStack(top);
            break;

        case BSHUnaryExpression.DECR:
            this.assertIsNumber(top);
            opcode = top.toASMType().getOpcode(Opcodes.IADD);
            this.pushOne(host, state);
            host.visitInsn(top.toASMType().getOpcode(Opcodes.INEG));
            host.visitInsn(opcode);
            state.popStack();
            state.popStack();
            state.pushStack(top);
            break;

        case BSHUnaryExpression.MINUS:
            this.assertIsNumber(top);
            host.visitInsn(top.toASMType().getOpcode(Opcodes.INEG));
            break;

        case BSHUnaryExpression.TILDE:
            this.assertIsNumber(top);
            host.visitInsn(top.toASMType().getOpcode(Opcodes.IXOR));
            break;

        case BSHUnaryExpression.BANG:
            this.assertIsBoolean(top);
            this.inverseBoolean(host, state);
            break;
        default:
            throw new CompilationAssertionFailed("Not yet implememented");
        }
    }

    /**
     * Inverte o boolean do stack
     * 
     * @param mv {@link MethodVisitor}
     * @param state {@link CompilationState}
     */
    private void inverseBoolean(MethodVisitor mv, CompilationState state) {
        Label l2 = new Label();
        mv.visitJumpInsn(Opcodes.IFEQ, l2);
        mv.visitInsn(Opcodes.ICONST_0);
        Label l3 = new Label();
        mv.visitJumpInsn(Opcodes.GOTO, l3);
        mv.visitLabel(l2);
        mv.visitInsn(Opcodes.ICONST_1);
        mv.visitLabel(l3);
    }

    /**
     * Lanca uma excecao se no topo do stack nao tiver um boolean
     * 
     * @param top {@link TypeWrapper} o topo do stack
     * @throws CompilationFailedException e
     */
    private void assertIsBoolean(TypeWrapper top) throws CompilationFailedException {
        if (!JavaClassWrapper.BOOLEAN_WRAPPER.equals(top)) {
            throw new CompilationFailedException(this.node.getLineNumber(), "Boolean expected but found:  " + top);
        }
    }

    /**
     * Lanca uma excecao se o topo do stack nao for um number
     * 
     * @param wrapper {@link TypeWrapper}
     * @throws CompilationFailedException e
     */
    private void assertIsNumber(TypeWrapper wrapper) throws CompilationFailedException {
        if (!(this.unit.reflectionUtils().isIntPrimitive(wrapper) || wrapper.equals(JavaClassWrapper.DOUBLE_WRAPPER)
                || wrapper.equals(JavaClassWrapper.LONG_WRAPPER) || wrapper.equals(JavaClassWrapper.FLOAT_WRAPPER) || wrapper
                    .equals(JavaClassWrapper.BIG_DECIMAL_WRAPPER))) {
            throw new CompilationFailedException(this.node.getLineNumber(), "Number expected but found:  " + wrapper);
        }
    }

    /**
     * Empurra o 1 no stack. Usa o 1, 1f ou 1.0 dependendo do tipo que estiver no topo do stack
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     */
    private void pushOne(MethodVisitor host, CompilationState state) {
        TypeWrapper top = state.peekStack();
        state.pushStack(top);
        if (this.unit.reflectionUtils().isIntPrimitive(top)) {
            BytecodeHelper.pushIntConst(1, host);
            return;
        }
        Class<?> javaType = JavaClassWrapper.unwrapJavaClass(top);
        if (long.class.equals(javaType)) {
            BytecodeHelper.pushLongConst(1, host);
        } else if (float.class.equals(javaType)) {
            BytecodeHelper.pushFloatConst(1, host);
        } else if (double.class.equals(javaType)) {
            BytecodeHelper.pushDoubleConst(1, host);
        }
        throw new CompilationAssertionFailed("Not yet implememented");
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.NodeCompiler#nodeResultType(bsh.meta.Scope)
     */
    public TypeWrapper nodeResultType(Scope scope) throws CompilationFailedException {
        return this.unit.getNodeType(this.node.getChild(0), scope);
    }

}
